/**
* Copyright (c) 2003-2005 Fernando Dobladez
*
* This file is part of AntDoclet.
*
* AntDoclet is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* AntDoclet is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AntDoclet; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
package com.neuroning.antdoclet;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import org.apache.tools.ant.IntrospectionHelper;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.TaskContainer;
import org.apache.tools.ant.types.EnumeratedAttribute;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Tag;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
/**
* An object of this class represents a Java class that is: an Ant Task, or an
* Ant Type.
*
* It provides information about the Task/Type's attributes, nested elements and
* more.
*
* It's intended to be used for documenting Ant Tasks/Types
*
* @author Fernando Dobladez <dobladez@gmail.com>
*/
public class AntDoc implements Comparable {
/**
* An IntrospectionHelper (from Ant) to interpret ant-specific conventions
* from Tasks and Types
*/
private IntrospectionHelper introHelper;
/**
* Javadoc description for this type.
*/
private ClassDoc doc;
/**
* Javadoc "root node"
*/
private RootDoc rootdoc;
/**
* The java Class for this type
*/
private Class clazz;
private AntDoc(IntrospectionHelper ih, RootDoc rootdoc, ClassDoc doc,
Class clazz) {
this.doc = doc;
this.rootdoc = rootdoc;
this.introHelper = ih;
this.clazz = clazz;
}
public static AntDoc getInstance(String clazz) {
return getInstance(clazz, null);
}
public static AntDoc getInstance(Class clazz) {
return getInstance(clazz, null);
}
public static AntDoc getInstance(String clazz, RootDoc rootdoc) {
Class c = null;
try {
c = Class.forName(clazz);
} catch (Throwable ee) {
// try inner class (replacing last . for $)
int lastdot = clazz.lastIndexOf(".");
if (lastdot >= 0) {
String newName = clazz.substring(0, lastdot) + "$"
+ clazz.substring(lastdot + 1);
// System.out.println("trying inner:"+newName);
try {
c = Class.forName(newName);
} catch (Throwable e) {
System.err.println("WARNING: AntDoclet couldn't find '"
+ clazz
+ "'. Make sure it's in the CLASSPATH");
}
}
}
return c != null ? getInstance(c, rootdoc) : null;
}
public static AntDoc getInstance(Class clazz, RootDoc rootdoc) {
AntDoc d = null;
IntrospectionHelper ih = IntrospectionHelper.getHelper(clazz);
ClassDoc doc = null;
if (rootdoc != null) doc = rootdoc.classNamed(clazz.getName());
// Filter out those types/tasks that are marked as "ignored"
if (!"true".equalsIgnoreCase(Util.tagAttributeValue(doc, "ant.task",
"ignore"))
&& !"true".equalsIgnoreCase(Util.tagAttributeValue(doc, "ant.type",
"ignore"))) {
d = new AntDoc(ih, rootdoc, doc, clazz);
}
return d;
}
/**
* @return Whether this represents an Ant Task (otherwise, it is assumed as
* a Type)
*/
public boolean isTask() {
return Task.class.isAssignableFrom(this.clazz);
}
/**
* @return Is this an Ant Task Container?
*/
public boolean isTaskContainer() {
return TaskContainer.class.isAssignableFrom(this.clazz);
}
/**
* @return Should this task/type be excluded?
*/
public boolean isIgnored() {
return "true".equalsIgnoreCase(Util.tagAttributeValue(doc, "ant.task", "ignore"))
|| "true".equalsIgnoreCase(Util.tagAttributeValue(doc, "ant.type", "ignore"));
}
/**
* @return Is the source code for this type included in this javadoc run?
*/
public boolean sourceIncluded() {
return doc != null ? doc.isIncluded() : false;
}
/**
*
* @return The source comment (description) for this class (task/type)
*/
public String getComment() {
return doc != null ? doc.commentText() : null;
}
/**
* @return Short comment for this class (basically, the first sentence)
*/
public String getShortComment() {
if (doc == null) return null;
Tag[] firstTags = doc.firstSentenceTags();
if (firstTags.length > 0 && firstTags[0] != null)
return firstTags[0].text();
return null;
}
/**
* Get the attributes in this class from Ant's point of view.
*
* @return Collection of Ant attributes, excluding those inherited from
* org.apache.tools.ant.Task, or null if there are none
*/
public Collection getAttributes() {
ArrayList attrs = Collections.list(introHelper.getAttributes());
if (attrs.isEmpty()) return null;
else {
// filter out all attributes inherited from Task, since they are
// common to all Ant Tasks and tend to confuse
try {
BeanInfo beanInfo = Introspector.getBeanInfo(Task.class);
PropertyDescriptor[] commonProps = beanInfo
.getPropertyDescriptors();
for (int i = 0; i < commonProps.length; i++) {
String propName = commonProps[i].getName().toLowerCase();
// System.out.println("Ignoring task property:"+propName);
attrs.remove(propName);
}
} catch (Exception e) {
e.printStackTrace();
}
return attrs;
}
}
/**
*
* @return a collection of the "Nested Elements" that this Ant tasks accepts, or null if there are none
*/
public Iterator<String> getNestedElements() {
Enumeration elements = introHelper.getNestedElements();
if (elements.hasMoreElements())
{
Collection c = new HashSet<String>();
while (elements.hasMoreElements()) {
c.add(elements.nextElement());
}
return c.iterator();
}
else {
return null;
}
}
/**
* Get the extension points for this class.
* Derived from the add(instance) or addConfigured(instance) methods.
* Each class is technically an Ant type, but typically you wont want to document it; just its concrete implementations.
* You can pass these strings to getImplementingClasses() to finds the available implementations.
* @return The fully qualified class names, or null if there are none
*/
public Iterator<String> getNestedTypes() {
List<Method> mm = introHelper.getExtensionPoints();
if (mm.isEmpty()) {
return null;
}
else {
Collection c = new HashSet<String>();
for(Method m:mm)
{
String classname = m.getParameterTypes()[0].getName();
c.add(classname);
}
return c.iterator();
}
}
/**
* Find all subclasses of the given abstract class or interface.
* Does NOT match the class itself.
*/
public Iterator<String> getImplementingClasses(String className)
{
List<String> imps = new ArrayList<String>();
ClassDoc thisClass = rootdoc.classNamed(className);
for (ClassDoc cd : rootdoc.classes()) {
if (cd.subclassOf(thisClass) && !cd.qualifiedName().equals(className)) {
imps.add(cd.qualifiedName());
}
}
return imps.iterator();
}
/**
* Get the AntDoc for the specified (arbitrary) class.
*
* @param className
* @return null if the class cannot be found on the classpath.
*/
public AntDoc getTypeDoc(String className)
{
return getInstance(className, rootdoc);
}
/**
* Get the comment for the add or addconfigured method for the specified class (extension).
*
* @return The source comment (description), or null if the class cannot be found.
*/
public String getCommentForType(String type) {
notNull(type, "type");
MethodDoc m = getMethodForType(doc, type);
return m==null ? null : m.commentText();
}
public String getFullClassName() {
return clazz.getName();
}
/**
*
* @return true if this refers to an inner-class
*/
public boolean isInnerClass() {
if (doc == null) return false;
boolean inner = (doc.containingClass() != null);
return inner;
}
/**
* Get the comment about the requirement of this attribute. The comment if
* extracted from the
*
* @ant.required tag
* @param attribute
* @return A comment. A null String if this attribute is not declared as
* required
*/
public String getAttributeRequired(String attribute) {
MethodDoc method = getMethodFor(this.doc, attribute);
if (method == null) {
return null;
}
return Util.tagValue(method, "ant.required");
}
/**
* Get the comment about the "non-requirement" of this attribute. The
* comment if extracted from the
*
* @ant.not-required tag
* @param attribute
* @return A comment. A null String if this attribute is not declared as
* non-required
*/
public String getAttributeNotRequired(String attribute) {
MethodDoc method = getMethodFor(this.doc, attribute);
if (method == null) {
return null;
}
return Util.tagValue(method, "ant.not-required");
}
/**
* Return the "category" of this Ant "task" or "type"
*
* @returns The value of the "category" attribute of the
* @ant.task or
* @ant.type if it exists.
*
*/
public String getAntCategory() {
String antCategory = Util.tagAttributeValue(this.doc, "ant.task",
"category");
if (antCategory == null)
antCategory = Util.tagAttributeValue(this.doc, "ant.type",
"category");
if (antCategory == null && getContainerDoc() != null)
antCategory = getContainerDoc().getAntCategory();
return antCategory;
}
/**
* @returns true if the class has a
* @ant.type or
* @ant.task tag in it
*/
public boolean isTagged() {
return Util.tagAttributeValue(this.doc, "ant.task", "name") != null
|| Util.tagAttributeValue(this.doc, "ant.type", "name") != null;
}
/**
* Return the name of this type from Ant's perspective
*
* @returns The value of the
* @ant.task or
* @ant.type if it exists. Otherwise, the Java class name.
*
*/
public String getAntName() {
String antName = Util.tagAttributeValue(this.doc, "ant.task", "name");
if (antName == null)
antName = Util.tagAttributeValue(this.doc, "ant.type", "name");
// Handle inner class case
if (antName == null && getContainerDoc() != null) {
antName = getContainerDoc().getAntName()
+ "."
+ this.clazz
.getName()
.substring(
this.clazz.getName().lastIndexOf('$') + 1);
}
if (antName == null) antName = typeToString(this.clazz);
return antName;
}
/**
*
* @see #getNestedElements()
* @param elementName
* @return The java type for the specified element accepted by this task
*/
public Class getElementType(String elementName) {
return introHelper.getElementType(elementName);
}
/**
* Return a new AntDoc for the given "element"
*/
public AntDoc getElementDoc(String elementName) {
return getInstance(getElementType(elementName), this.rootdoc);
}
/**
* Return a new AntDoc for the "container" of this type. Only makes sense
* for inner classes.
*
*/
public AntDoc getContainerDoc() {
if (!isInnerClass()) return null;
return getInstance(this.doc.containingClass().qualifiedName(),
this.rootdoc);
}
/**
* Return the name of the type for the specified attribute
*/
public String getAttributeType(String attributeName) {
return typeToString(introHelper.getAttributeType(attributeName));
}
/**
* Retrieves the method comment for the given attribute.
* The comment of the setter is used preferably to the getter comment.
*
* @param attribute
* @return The comment for the specified attribute
*/
public String getAttributeComment(String attribute) {
MethodDoc method = getMethodFor(this.doc, attribute);
if (method == null) {
return new String();
}
return method.commentText();
}
/**
* Searches the given class for the appropriate setter or getter for the given attribute.
* This method only returns the getter if no setter is available.
* If the given class provides no method declaration, the superclasses are
* searched recursively.
*
* @param attribute
* @param methods
* @return The MethodDoc for the given attribute or null if not found
*/
private static MethodDoc getMethodFor(ClassDoc classDoc, String attribute) {
if (classDoc == null) {
return null;
}
MethodDoc result = null;
MethodDoc[] methods = classDoc.methods();
for (int i = 0; i < methods.length; i++) {
// we give priority to the documentation on the "setter" method of
// the attribute
// but if the documentation is only on the "getter", use it
// we give priority to the documentation on the "setter" method of
// the attribute
// but if the documentation is only on the "getter", use it
if (methods[i].name().equalsIgnoreCase("set" + attribute)) {
return methods[i];
} else if (methods[i].name().equalsIgnoreCase("get" + attribute)) {
result = methods[i];
}
}
if (result == null) {
return getMethodFor(classDoc.superclass(), attribute);
}
return result;
}
/**
* Searches the given class for the appropriate setter or getter for the given attribute.
* This method only returns the getter if no setter is available.
* If the given class provides no method declaration, the superclasses are
* searched recursively.
*
* @param attribute
* @param methods
* @return The MethodDoc for the given attribute or null if not found
*/
private static MethodDoc getMethodForType(ClassDoc classDoc, String nestedType) {
notNull(classDoc, "classDoc");
notNull(nestedType, "nestedType");
MethodDoc result = null;
MethodDoc[] methods = classDoc.methods();
for (MethodDoc method : methods) {
if (method.name().equalsIgnoreCase("add")||method.name().equalsIgnoreCase("addConfigured")) {
com.sun.javadoc.Parameter[] params = method.parameters();
if (params.length == 1) {
// Ugly. I have the method, why can't Javadoc give me the comment directly?
if (nestedType.endsWith(params[0].type().typeName())) {
result = method;
break;
}
}
}
}
if (result == null && classDoc.superclass() != null) {
return getMethodForType(classDoc.superclass(), nestedType);
}
return result;
}
/**
* characters</echo>
*
* @return true if this Ant Task/Type expects characters in the element
* body.
*/
public boolean supportsCharacters() {
return introHelper.supportsCharacters();
}
// Private helper methods:
/**
* Create a "displayable" name for the given type
*
* @param clazz
* @return a string with the name for the given type
*/
private static String typeToString(Class clazz) {
String fullName = clazz.getName();
String name = fullName.lastIndexOf(".") >= 0 ? fullName
.substring(fullName.lastIndexOf(".") + 1) : fullName;
String result = name.replace('$', '.'); // inner's
// use
// dollar
// signs
if (EnumeratedAttribute.class.isAssignableFrom(clazz)) {
try {
EnumeratedAttribute att = (EnumeratedAttribute) clazz
.newInstance();
result = "String [";
String[] values = att.getValues();
result += "\"" + values[0] + "\"";
for (int i = 1; i < values.length; i++)
result += ", \"" + values[i] + "\"";
result += "]";
} catch (java.lang.IllegalAccessException iae) {
// ignore, may a protected/private Enumeration
}
catch(Exception e) {
e.printStackTrace();
}
}
return result;
}
public int compareTo(Object o) {
AntDoc otherDoc = (AntDoc)o;
String fullName1 = getAntCategory() +":" + getAntName();
String fullName2 = otherDoc.getAntCategory() +":"+ otherDoc.getAntName();
return fullName1.compareTo(fullName2);
}
/**
* Argument check for methods - not nullable.
*
* Typed, so you can use instancevar = notNull(arg,"arg");
*
* @param <T>
* @param t
* @param msg Message for
* @throws NullPointerException if t is null
* @return
*/
public static <T> T notNull(T t, String msg) {
if (t == null) {
throw new NullPointerException(msg);
}
return t;
}
}